﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Media;
using Microsoft.Phone.Tasks;

namespace Tequila.Phone.Controls
{
    /// <summary>
    /// Simple <see cref="RichTextBox">RichTextBox</see>-based document for rich content presentation.
    /// </summary>
    public class FlowDocument : ContentControl
    {
        private const string ParagraphContainerStylePropertyName = "ParagraphContainerStyle";

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty ContentPaddingProperty =
            DependencyProperty.Register("ContentPadding",
                                        typeof(Thickness),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(new Thickness(0)));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty ContentMarginProperty =
            DependencyProperty.Register("ContentMargin",
                                        typeof (Thickness),
                                        typeof (FlowDocument),
                                        new PropertyMetadata(new Thickness(0)));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty ParagraphOffsetProperty =
            DependencyProperty.Register("ParagraphOffset",
                                        typeof(Thickness),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(new Thickness(0)));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty ParagraphContainerStyleProperty =
            DependencyProperty.Register(ParagraphContainerStylePropertyName,
                                        typeof (Style),
                                        typeof (FlowDocument),
                                        new PropertyMetadata(null));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty XamlDocumentProperty =
            DependencyProperty.Register("XamlDocument",
                                        typeof(string),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(OnDocumentChanged));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty XamlDocumentIfParsingFailedProperty =
            DependencyProperty.Register("XamlDocumentIfParsingFailed",
                                        typeof(string),
                                        typeof(FlowDocument),
                                        null);

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty ParsingErrorColorProperty =
            DependencyProperty.Register("ParsingErrorColor",
                                        typeof(Color),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(Colors.Red));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty BlockParsingFailedMessageProperty =
            DependencyProperty.Register("BlockParsingFailedMessage",
                                        typeof(string),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(null));

        /// <summary>
        /// <see cref="DependencyProperty">Dependency Property</see>.
        /// </summary>
        public static readonly DependencyProperty DocumentParsingFailedMessageProperty =
            DependencyProperty.Register("DocumentParsingFailedMessage",
                                        typeof(string),
                                        typeof(FlowDocument),
                                        new PropertyMetadata(null));

        private ItemsControl _container;
        private ScrollViewer _scrollViewer;
        private readonly Binding _fontSizeBinding;
        private readonly Binding _fontFamilyBinding;
        private readonly Binding _paragraphOffsetBinding;
        private readonly Binding _foregroundBinding;

        /// <summary>
        /// Initializes new instance of <see cref="FlowDocument"/> class.
        /// </summary>
        public FlowDocument()
        {
            DefaultStyleKey = typeof (FlowDocument);

            _foregroundBinding = new Binding
                                     {
                                         Source = this,
                                         Path = new PropertyPath("Foreground")
                                     };

            _fontSizeBinding = new Binding
                                   {
                                       Source = this,
                                       Path = new PropertyPath("FontSize")
                                   };

            _fontFamilyBinding = new Binding
                                     {
                                         Source = this,
                                         Path = new PropertyPath("FontFamily")
                                     };

            _paragraphOffsetBinding = new Binding
                                          {
                                              Source = this,
                                              Path = new PropertyPath("ParagraphOffset")
                                          };
        }

        private static void OnDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is FlowDocument)) return;

            (d as FlowDocument).UpdateDocument(e.NewValue as string);
        }

        /// <summary>
        /// Gets or sets Paragraph container style.
        /// </summary>
        [Obsolete]
        public Style ParagraphContainerStyle
        {
            get { return GetValue(ParagraphContainerStyleProperty) as Style; }
            set { SetValue(ParagraphContainerStyleProperty, value); }
        }

        /// <summary>
        /// Gets or sets formatted XAML document.
        /// </summary>
        public string XamlDocument
        {
            get { return GetValue(XamlDocumentProperty) as string; }
            set { SetValue(XamlDocumentProperty, value); }
        }

        /// <summary>
        /// Gets or sets formatted XAML document which is being displayed when XamlDocument parsing failed.
        /// </summary>
        public string XamlDocumentIfParsingFailed
        {
            get { return GetValue(XamlDocumentIfParsingFailedProperty) as string; }
            set { SetValue(XamlDocumentIfParsingFailedProperty, value); }
        }

        /// <summary>
        /// Gets or sets <see cref="Thickness">padding</see> of content.
        /// </summary>
        public Thickness ContentPadding
        {
            get { return (Thickness)GetValue(ContentPaddingProperty); }
            set { SetValue(ContentPaddingProperty, value); }
        }

        /// <summary>
        /// Gets or sets <see cref="Thickness">margin</see> of content.
        /// </summary>
        public Thickness ContentMargin
        {
            get { return (Thickness)GetValue(ContentMarginProperty); }
            set { SetValue(ContentMarginProperty, value); }
        }

        /// <summary>
        /// Gets or sets paragraph <see cref="Thickness">offset</see> of content.
        /// </summary>
        public Thickness ParagraphOffset
        {
            get { return (Thickness)GetValue(ParagraphOffsetProperty); }
            set { SetValue(ParagraphOffsetProperty, value); }
        }

        /// <summary>
        /// Gets or sets parsing error <see cref="Color">color</see>.
        /// </summary>
        public Color ParsingErrorColor
        {
            get { return (Color)GetValue(ParsingErrorColorProperty); }
            set { SetValue(ParsingErrorColorProperty, value); }
        }

        /// <summary>
        /// Gets or sets message which is being displayed when XamlDocument single <see cref="Block">block</see> parsing failed instead of that block.
        /// </summary>
        public string BlockParsingFailedMessage
        {
            get { return GetValue(BlockParsingFailedMessageProperty) as string; }
            set { SetValue(BlockParsingFailedMessageProperty, value); }
        }

        /// <summary>
        /// Gets or sets message which is being displayed when XamlDocument document parsing failed instead of that document (if XamlDocumentIfParsingFailed not specified).
        /// </summary>
        public string DocumentParsingFailedMessage
        {
            get { return GetValue(DocumentParsingFailedMessageProperty) as string; }
            set { SetValue(DocumentParsingFailedMessageProperty, value); }
        }

        private static bool TryGetBlocks(string content, out List<Block> blocks)
        {
            blocks = new List<Block>();

            try
            {
                var section = XamlReader.Load(content) as Section;

                if (section == null)
                {
                    return true;
                }

                blocks.AddRange(section.Blocks.ToList());

                section.Blocks.Clear();

                return true;
            }
            catch (XamlParseException ex)
            {
                System.Diagnostics.Debug.WriteLine(ex);
            }

            return false;
        }

        private Hyperlink _previousClickTarget;
        private bool _isHyperlinking;
        private readonly object _locker = new object();

        private void HyperlinkClick(object sender, RoutedEventArgs e)
        {
            if (!_isHyperlinking)
            {
                lock (_locker)
                {
                    if (!_isHyperlinking)
                    {
                        _isHyperlinking = true;

                        try
                        {
                            var hl = (sender as Hyperlink);

                            if (hl != null && !Equals(_previousClickTarget, hl))
                            {
                                hl.Click -= HyperlinkClick;
                                var uri = hl.NavigateUri;

                                hl.NavigateUri = null;

                                if (uri == null || !uri.IsAbsoluteUri) return;

                                var task = new WebBrowserTask {Uri = uri};
                                task.Show();
                            }
                        }
                        catch(Exception ex)
                        {
                            System.Diagnostics.Debug.WriteLine(ex);
                        }

                        _isHyperlinking = false;
                    }
                }
            }
        }

        private void UpdateDocument(string content)
        {
            if (_container != null)
            {
                foreach (var item in _container.Items)
                {
                    if (!(item is RichTextBox)) continue;

                    var rtb = (item as RichTextBox);

                    foreach (var p in rtb.Blocks.OfType<Paragraph>())
                    {
                        p.Inlines.Clear();
                    }

                    rtb.Blocks.Clear();
                }
                
                _container.Items.Clear();
            }
            else
            {
                return;
            }

            if (_scrollViewer != null)
            {
                _scrollViewer.ScrollToVerticalOffset(0);
                _scrollViewer.InvalidateScrollInfo();
            }

            if (string.IsNullOrEmpty(content))
            {
                return;
            }

            List<Block> blocks;

            if (!TryGetBlocks(content, out blocks))
            {
                if (string.IsNullOrEmpty(XamlDocumentIfParsingFailed) ||
                    !TryGetBlocks(XamlDocumentIfParsingFailed, out blocks))
                {
                    var paragraph = new Paragraph();
                    paragraph.Inlines.Add(BuildErrorMessage(DocumentParsingFailedMessage));
                    blocks.Add(paragraph);
                }
            }

            foreach (var block in blocks)
            {
                try
                {
                    var rtb = BuildParagraph();
                    rtb.Blocks.Add(block);
                    _container.Items.Add(rtb);

                    continue;
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine(ex);
                }

                if (!string.IsNullOrEmpty(BlockParsingFailedMessage))
                {
                    var paragraph = new Paragraph();
                    paragraph.Inlines.Add(BuildErrorMessage(BlockParsingFailedMessage));
                    blocks.Add(paragraph);
                }
            }

            foreach (var item in _container.Items)
            {
                if (!(item is RichTextBox)) continue;

                foreach (var p in (item as RichTextBox).Blocks.OfType<Paragraph>())
                {
                    foreach (var hl in p.Inlines.OfType<Hyperlink>())
                    {
                        hl.Click += HyperlinkClick;
                    }
                }
            }

            _previousClickTarget = null;
        }

        private Span BuildErrorMessage(string message)
        {
            var error = new Bold { Foreground = new SolidColorBrush(ParsingErrorColor) };

            if (!string.IsNullOrEmpty(message))
            {
                error.Inlines.Add(message);
            }

            return error;
        }

        private RichTextBox BuildParagraph()
        {
            var rtb = new FlowDocumentElement();

            rtb.SetBinding(ForegroundProperty, _foregroundBinding);
            rtb.SetBinding(FontSizeProperty, _fontSizeBinding);
            rtb.SetBinding(FontFamilyProperty, _fontFamilyBinding);
            rtb.SetBinding(MarginProperty, _paragraphOffsetBinding);

            return rtb;
        }

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass) call <see cref="M:System.Windows.Controls.Control.ApplyTemplate"/>. In simplest terms, this means the method is called just before a UI element displays in an application. For more information, see Remarks.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _container = GetTemplateChild("Container") as ItemsControl;

            _scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;

            UpdateDocument(XamlDocument);
        }
    }
}
